#! /bin/bash
#
# Routine sanity checks for libpqxx source tree.
#
# Optionally, set environment variable "srcdir" to the source directory.  It
# defaults to the parent directory of the one where this script is.  This trick
# requires bash (or a close equivalent) as the shell.

set -eu -o pipefail

SRCDIR="${srcdir:-$(dirname "${BASH_SOURCE[0]}")/..}"
PQXXVERSION="$(cd "$SRCDIR" && "$SRCDIR/tools/extract_version")"

ARGS="${1:-}"


set_up() {
    MY_VENV="$(mktemp -d)"
    if [ -z "$MY_VENV" ]
    then
        echo "Failed to set up virtualenv." >&2
        exit 2
    fi
    virtualenv -q "$MY_VENV"
    # shellcheck disable=SC1091
    . "$MY_VENV/bin/activate"
    pip install -q pyflakes ruff shellcheck-py
}


clean_up() {
    if [ -n "${MY_VENV:-}" ] && [ "${VIRTUAL_ENV:-x}" = "${MY_VENV:-y}" ]
    then
        deactivate || true
    fi
    if [ -n "${MY_VENV:-}" ] && [ -d "${MY_VENV:-/nonexistent/dir}" ]
    then
        rm -r "$MY_VENV"
    fi
}


trap clean_up EXIT
set_up


# Check that all source code is ASCII.
#
# I'd love to have rich Unicode, but I can live without it.  But we don't want
# any surprises in contributions.
check_ascii() {
    local exotics
    exotics="$(
        find "$SRCDIR" -type f -name '*.[ch].cxx' -print0 |
        xargs -0 cat |
        tr -d '\011-\176' |
        wc -c
    )"
    if [ "$exotics" != 0 ]
    then
        echo >&2 "There's a non-ASCII character somewhere."
        exit 1
    fi
}


# This version must be at the top of the NEWS file.
check_news_version() {
    if ! head -n1 "$SRCDIR/NEWS" | grep -q "^$PQXXVERSION\$"
    then
        cat <<EOF >&2
Version $PQXXVERSION is not at the top of NEWS.
EOF
        exit 1
    fi
}


# Count number of times header $1 is included from each of given input files.
# Output is lines of <filename>:<count>, one line per file, sorted.
count_includes() {
    local HEADER_NAME PAT
    HEADER_NAME="$1"
    shift
    PAT='^[[:space:]]*#[[:space:]]*include[[:space:]]*[<"]'"$HEADER_NAME"'[>"]'
    # It's OK for the grep to fail.
    find "$SRCDIR/include/pqxx" -type f -print0 |
        ( xargs -0 grep -c "$PAT" || true ) |
        sort
}


# Check that any includes of $1-pre.hxx are matched by $1-post.hxx ones.
match_pre_post_headers() {
    local NAME TEMPDIR PRE POST
    NAME="$1"
    TEMPDIR="$MY_VENV/tmp-lint"
    mkdir -p  "$TEMPDIR"
    PRE="$TEMPDIR/pre"
    POST="$TEMPDIR/post"
    count_includes "$SRCDIR/$NAME-pre.hxx" >"$PRE"
    count_includes "$SRCDIR/$NAME-post.hxx" >"$POST"
    DIFF="$(diff "$PRE" "$POST")" || true
    rm -r -- "$TEMPDIR"
    if test -n "$DIFF"
    then
        cat <<EOF >&2
Mismatched pre/post header pairs:

$DIFF
EOF
        exit 1
    fi
}


# Any file that includes header-pre.hxx must also include header-post.hxx, and
# vice versa.  Similar for ignore-deprecated-{pre|post}.hxx.
check_compiler_internal_headers() {
    match_pre_post_headers "pqxx/internal/header"
    match_pre_post_headers "pqxx/internal/ignore-deprecated"
}


check_yaml() {
    yamllint .
}


cpplint() {
    local dialect includes

    pip install -q clang-tidy
    if [ -e compile_flags ]
    then
        # Pick out relevant flags, but leave out the rest.
        # If we're not compiling with clang, compile_flags may contain
        # options that clang-tidy doesn't recognise.
        dialect="$(grep -o -- '-std=[^[:space:]]*' compile_flags || true)"
        includes="$(
            grep -o -- '-I[[:space:]]*[^[:space:]]*' compile_flags ||
            true)"
    else
        dialect=""
        includes=""
    fi

# TODO: Please, is there any way we can parallelise this?
# TODO: Check test/, but tolerate some of the dubious stuff tests do.
    # shellcheck disable=SC2086,2046
    clang-tidy "$SRCDIR"/src/*.cxx "$SRCDIR"/tools/*.cxx \
        -- \
        -I"$SRCDIR/include" -Iinclude $dialect $includes

    # Run Facebook's "infer" static analyser, if available.
    # A "pip install" didn't work for me: No module named 'nltk'.
    # Instructions here: https://fbinfer.com/docs/getting-started/
    if which infer >/dev/null
    then
        # This will work in an out-of-tree build, but either way it does
        # require a successful "configure", or a cmake with the "make"
        # generator.
        infer capture -- make -j"$(nproc)"
        infer run
    fi
}


pylint() {
    pyflakes "$SRCDIR"/tools/*.py
    ruff check "$SRCDIR"/tools/*.py
}


shelllint() {
    local TLS="deprecations extract_version format lint todo update-copyright"
    shellcheck "$SRCDIR/autogen.sh"
    for s in $TLS
    do
        shellcheck "$SRCDIR/tools/$s"
    done
}


mdlint() {
    if which mdl >/dev/null
    then
        find . -name \*.md -exec mdl -c .markdownlint.yaml '{}' ';'
    fi
}


main() {
    local full="no"
    for arg in $ARGS
    do
        case $arg in
        -h|--help)
            cat <<EOF
Perform static checks on libpqxx build tree.

Usage:
    $0 -h|--help -- print this message and exit.
    $0 -f|--full -- perform full check, including C++ analysis.
    $0 -- perform default check.
EOF
            exit 0
        ;;
        -f|--full)
            full="yes"
        ;;
        *)
            echo >&2 "Unknown argument: '$arg'"
            exit 1
        ;;
        esac
    done

    check_ascii
    pylint
    shelllint
    check_news_version
    check_compiler_internal_headers
    mdlint
    check_yaml
    if [ $full == "yes" ]
    then
        cpplint
    fi
}


main
